package com.globefrog.framework.log;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.spi.ILoggingEvent;
import com.globefrog.framework.common.constant.LogConstants;
import com.globefrog.framework.log.util.IOUtils;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.MDC;

/**
 * 日志事件处理器.
 *
 * @author caijinbang
 * @date 2020-11-05 21:04
 */
@Slf4j
public class LogEventProcessor {


  /**
   * 最大hold大小.
   */
  private static final int MAX_HOLD_SIZE = 5000;
  /**
   * 日志输出格式.
   */
  private PatternLayout layout;

  /**
   * 日志目录.
   */
  private String logFolder;
  /**
   * 日志事件队列.
   */
  private Queue<ILoggingEvent> logEventQueue = new ConcurrentLinkedQueue<>();
  /**
   * 事件大小.
   */
  private AtomicInteger eventSize = new AtomicInteger(0);
  /**
   * 锁.
   */
  private final Lock lock = new ReentrantLock();
  /**
   * 是否hold住.
   */
  private volatile boolean hold = true;
  /**
   * writer.
   */
  private volatile Writer writer;

  public LogEventProcessor(PatternLayout layout, String logFolder) {
    this.layout = layout;
    this.logFolder = logFolder;
  }

  /**
   * 处理日志事件.
   *
   * @param event 事件
   * @throws IOException IO异常
   */
  public void process(ILoggingEvent event) throws IOException {
    if (hold) {
      // 添加事件
      addEvent(event);
      // 判断是否写入traceLog,前提是WARN级别或则日志过大
      if (flushLog(event)) {
        flushTraceLogs();
        hold = false;
      }
    } else {
      //当前状态不为hold直接写入
      writeEventLog(event);
    }
  }

  private void addEvent(ILoggingEvent event) {
    logEventQueue.add(event);
    eventSize.getAndIncrement();
  }

  /**
   * 是否需要输出日志.
   *
   * @param event 日志事件
   * @return 是否输出日志
   */
  private boolean flushLog(ILoggingEvent event) {
    // 只有WARN级别的日志或者事件大小超过5000才输出
    return event.getLevel().isGreaterOrEqual(Level.WARN) || eventSize.get() > MAX_HOLD_SIZE;
  }

  /**
   * 输出追踪日志.
   *
   * @throws IOException IO异常
   */
  private void flushTraceLogs() throws IOException {
    try {
      // 防止并发
      lock.lock();
      if (writer == null) {
        // 创建文件和writer
        writer = createWriter();
      }
      while (true) {
        // 拉取所有的日志事件
        ILoggingEvent event = logEventQueue.poll();
        if (event == null) {
          return;
        }
        writeEventLog(event);
      }
    } finally {
      lock.unlock();
    }
  }

  private void writeEventLog(ILoggingEvent event) throws IOException {
    // doLayout 主要负责组装需要上报的信息
    String log = layout.doLayout(event);
    writer.write(log);
  }

  /**
   * 创建Writer.
   *
   * @return Writer
   * @throws FileNotFoundException 文件未找到异常
   */
  private Writer createWriter() throws FileNotFoundException {
    if (logFolder == null) {
      return new BufferedWriter(new OutputStreamWriter(System.err, StandardCharsets.UTF_8));
    } else {
      // 日志文件路径
      String logPath = generateLogFilePath(getAction(), new Date(), getRequestId());
      ActionLog actionLog = ActionLogImpl.getInstance().currentActionLog();
      if (actionLog != null) {
        // 设置TraceLog输出路径
        actionLog.setTraceLogPath(logPath);
      }
      // 创建TraceLog日志文件
      File logFile = new File(logPath);
      // 创建父级目录
      createParentFolder(logFile);
      FileOutputStream fos = new FileOutputStream(logFile, true);
      OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
      return new BufferedWriter(outputStreamWriter);
    }
  }

  /**
   * 创建父级目录.
   *
   * @param logFile 日志文件
   */
  private void createParentFolder(File logFile) {
    File folder = logFile.getParentFile();
    if (!folder.mkdirs()) {
      log.debug("make dirs err. maybe exists");
    }
  }

  /**
   * 清空日志.
   *
   * @param forceFlushTraceLog 是否强制输出日志
   * @throws IOException IO异常
   */
  public void cleanup(boolean forceFlushTraceLog) throws IOException {
    if (forceFlushTraceLog) {
      flushTraceLogs();
    }
    if (logFolder == null) {
      IOUtils.flush(writer);
    } else {
      IOUtils.close(writer);
    }
  }

  /**
   * 从MDC中获取请求id.
   *
   * @return 请求id
   */
  private String getRequestId() {
    String requestId = MDC.get(LogConstants.MDC_REQUEST_ID);
    return requestId == null ? "unknown" : requestId;
  }

  /**
   * 从MDC中获取action.
   *
   * @return action
   */
  private String getAction() {
    return MDC.get(LogConstants.MDC_ACTION) == null ? "unknown" : MDC.get(LogConstants.MDC_ACTION);
  }

  /**
   * 生成日志文件路径,/日志文件路径/年/月/日志/操作名称/时间点.请求id.5位随机字母数字字符串.log
   *
   * @param action     动作
   * @param targetDate 目标日期
   * @param requestId  请求id
   * @return 路径
   */
  private String generateLogFilePath(String action, Date targetDate, String requestId) {
    String sequence = RandomStringUtils.randomAlphanumeric(5);
    String logPathFormat = "%1$s/%2$tY/%2$tm/%2$td/%3$s/%2$tH%2$tM.%4$s.%5$s.log";
    return String.format(logPathFormat, logFolder, targetDate, action, requestId, sequence);
  }
}
